/*
 * Copyright (C) 2023 Red Hat, Inc.
 *
 * SPDX-License-Identifier: Apache-2.0 OR MIT
 */
use crate::cxxrsutil::*;
use crate::ffiutil;

use anyhow::{Context, Result};
use cap_std::fs::{Dir, Permissions, PermissionsExt};
use cap_std_ext::dirext::CapStdExtDirExt;
use fn_error_context::context;
use std::collections::BTreeMap;
use std::fmt::Write;
use std::path::Path;

const TMPFILESD: &str = "usr/lib/tmpfiles.d";
const RPMOSTREE_TMPFILESD: &str = "usr/lib/rpm-ostree/tmpfiles.d";
const AUTOVAR_PATH: &str = "rpm-ostree-autovar.conf";

#[context("Deduplicate tmpfiles entries")]
pub fn deduplicate_tmpfiles_entries(tmprootfs_dfd: i32) -> CxxResult<()> {
    let tmprootfs_dfd = unsafe { ffiutil::ffi_dirfd(tmprootfs_dfd)? };

    // scan all rpm-ostree auto generated entries and save
    let tmpfiles_dir = tmprootfs_dfd
        .open_dir_optional(RPMOSTREE_TMPFILESD)
        .context(RPMOSTREE_TMPFILESD)?;
    let mut rpmostree_tmpfiles_entries = if let Some(tmpfiles_dir) = tmpfiles_dir {
        read_tmpfiles(&tmpfiles_dir)?
    } else {
        Default::default()
    };

    // remove autovar.conf first, then scan all system entries and save
    let tmpfiles_dir = if let Some(d) = tmprootfs_dfd
        .open_dir_optional(TMPFILESD)
        .context(TMPFILESD)?
    {
        d
    } else {
        if !rpmostree_tmpfiles_entries.is_empty() {
            return Err(
                format!("No {TMPFILESD} directory found, but have tmpfiles to process").into(),
            );
        }
        // Nothing to do here
        return Ok(());
    };

    if tmpfiles_dir.try_exists(AUTOVAR_PATH)? {
        tmpfiles_dir.remove_file(AUTOVAR_PATH)?;
    }
    let system_tmpfiles_entries = read_tmpfiles(&tmpfiles_dir)?;

    // remove duplicated entries in auto-generated tmpfiles.d,
    // which are already in system tmpfiles
    for (key, _) in system_tmpfiles_entries {
        rpmostree_tmpfiles_entries.retain(|k, _value| k != &key);
    }

    {
        // save the noduplicated entries
        let mut entries = String::from("# This file was generated by rpm-ostree.\n");
        for (_key, value) in rpmostree_tmpfiles_entries {
            writeln!(entries, "{value}").unwrap();
        }

        let perms = Permissions::from_mode(0o644);
        tmpfiles_dir.atomic_write_with_perms(&AUTOVAR_PATH, entries.as_bytes(), perms)?;
    }
    Ok(())
}

/// Read all tmpfiles.d entries in the target directory, and return a mapping
/// from (file path) => (single tmpfiles.d entry line)
#[context("Read systemd tmpfiles.d")]
fn read_tmpfiles(tmpfiles_dir: &Dir) -> Result<BTreeMap<String, String>> {
    tmpfiles_dir
        .entries()?
        .filter_map(|name| {
            let name = name.unwrap().file_name();
            if let Some(extension) = Path::new(&name).extension() {
                if extension != "conf" {
                    return None;
                }
            } else {
                return None;
            }
            Some(
                tmpfiles_dir
                    .read_to_string(name)
                    .ok()?
                    .lines()
                    .filter(|s| !s.is_empty() && !s.starts_with('#'))
                    .map(|s| s.to_string())
                    .collect::<Vec<_>>(),
            )
        })
        .flatten()
        .map(|s| {
            let entry = tmpfiles_entry_get_path(s.as_str())?;
            anyhow::Ok((entry.to_string(), s))
        })
        .collect()
}

#[context("Scan tmpfiles entries and get path")]
fn tmpfiles_entry_get_path(line: &str) -> Result<&str> {
    line.split_whitespace()
        .nth(1)
        .ok_or_else(|| anyhow::anyhow!("Malformed tmpfiles.d entry ({line})"))
}

#[cfg(test)]
mod tests {
    use rustix::fd::AsRawFd;

    use super::*;
    #[test]
    fn test_tmpfiles_entry_get_path() {
        let cases = [
            ("z /dev/kvm          0666 - kvm -", "/dev/kvm"),
            ("d /run/lock/lvm 0700 root root -", "/run/lock/lvm"),
            ("a+      /var/lib/tpm2-tss/system/keystore   -    -    -     -           default:group:tss:rwx", "/var/lib/tpm2-tss/system/keystore"),
        ];
        for (input, expected) in cases {
            let path = tmpfiles_entry_get_path(input).unwrap();
            assert_eq!(path, expected, "Input: {input}");
        }
    }

    fn newroot() -> Result<cap_std_ext::cap_tempfile::TempDir> {
        let root = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
        root.create_dir_all(RPMOSTREE_TMPFILESD)?;
        root.create_dir_all(TMPFILESD)?;
        Ok(root)
    }

    #[test]
    fn test_deduplicate_noop() -> Result<()> {
        let root = &newroot()?;
        deduplicate_tmpfiles_entries(root.as_raw_fd())?;
        Ok(())
    }

    // The first and 3rd are duplicates
    const PKG_FILESYSTEM_CONTENTS: &str = indoc::indoc! { r#"
d /var/cache 0755 root root - -
d /var/lib/games 0755 root root - -
d /var/tmp 1777 root root - -
d /var/spool/mail 0775 root mail - -
"# };
    const VAR_CONF: &str = indoc::indoc! { r#"
d /var/cache 0755 - - -
"# };
    const TMP_CONF: &str = indoc::indoc! { r#"
q /var/tmp 1777 root root 30d
"# };

    #[test]
    fn test_deduplicate() -> Result<()> {
        let root = &newroot()?;
        let rpmostree_tmpfiles_dir = root.open_dir(RPMOSTREE_TMPFILESD)?;
        let tmpfiles_dir = root.open_dir(TMPFILESD)?;
        rpmostree_tmpfiles_dir
            .atomic_write(format!("pkg-filesystem.conf"), PKG_FILESYSTEM_CONTENTS)?;
        tmpfiles_dir.atomic_write("var.conf", VAR_CONF)?;
        tmpfiles_dir.atomic_write("tmp.conf", TMP_CONF)?;
        assert!(!tmpfiles_dir.try_exists(AUTOVAR_PATH)?);
        deduplicate_tmpfiles_entries(root.as_raw_fd())?;
        let contents = tmpfiles_dir.read_to_string(AUTOVAR_PATH).unwrap();
        assert!(contents.contains("# This file was generated by rpm-ostree."));
        let entries = contents
            .lines()
            .filter(|l| !(l.is_empty() || l.starts_with('#')))
            .collect::<Vec<_>>();
        assert_eq!(entries.len(), 2);
        assert_eq!(entries[0], "d /var/lib/games 0755 root root - -");
        assert_eq!(entries[1], "d /var/spool/mail 0775 root mail - -");
        Ok(())
    }

    #[test]
    /// Verify that we no-op if the directories don't exist.
    fn test_deduplicate_emptydir() -> Result<()> {
        let root = &cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
        deduplicate_tmpfiles_entries(root.as_raw_fd())?;
        Ok(())
    }
}
